package scala.meta.internal.docstrings

import scala.annotation.tailrec
import scala.collection.Map
import scala.collection.Seq
import scala.collection.mutable
import scala.util.matching.Regex

import scala.meta.Position

/**
 * A fork of the Scaladoc parser in the Scala compiler with a few removed features.
 *
 * Removed features:
 * - linking to symbols
 * - reporting warnings
 */
object ScaladocParser {

  // Used in link() and for converting Javadoc @see to links
  private val LinkPattern = """(.+?#?.+?(\([^\)]*\)?)?)((?:\s+)(.*))?""".r

  /* Creates comments with necessary arguments */
  def createComment(
      body0: Option[Body] = None,
      authors0: List[Body] = List.empty,
      see0: List[Body] = List.empty,
      result0: Option[Body] = None,
      throws0: Map[String, Body] = Map.empty,
      valueParams0: Map[String, Body] = Map.empty,
      typeParams0: Map[String, Body] = Map.empty,
      version0: Option[Body] = None,
      since0: Option[Body] = None,
      todo0: List[Body] = List.empty,
      deprecated0: Option[Body] = None,
      note0: List[Body] = List.empty,
      example0: List[Body] = List.empty,
      constructor0: Option[Body] = None,
      inheritDiagram0: List[String] = List.empty,
      contentDiagram0: List[String] = List.empty,
      group0: Option[Body] = None,
      groupDesc0: Map[String, Body] = Map.empty,
      groupNames0: Map[String, Body] = Map.empty,
      groupPrio0: Map[String, Body] = Map.empty,
      hideImplicitConversions0: List[Body] = List.empty,
      shortDescription0: List[Body] = List.empty
  ): Comment =
    new Comment {
      val body = body0 getOrElse Body(Seq.empty)
      val authors = authors0
      val see = see0.map { body =>
        def convertTextToLink(inline: Inline): Inline = {
          inline match {
            case Chain(c) =>
              Chain(c.map(i => convertTextToLink(i)))
            case Summary(inline) =>
              // Make sure Javadoc text is converted into links
              Summary(inline match {
                case Text(text) =>
                  text match {
                    case LinkPattern(link, _, _, title) =>
                      Link(
                        link,
                        Option(title) map (Text.apply) getOrElse Text(link)
                      )
                    case text =>
                      Link(text, Text(text))
                  }
                case x => x
              })
            case x => x
          }
        }
        val blocks = body.blocks.map {
          case Paragraph(inline) =>
            Paragraph(convertTextToLink(inline))
          case x => x
        }
        body.copy(blocks)
      }
      val result = result0
      val throws = throws0
      val valueParams = valueParams0
      val typeParams = typeParams0
      val version = version0
      val since = since0
      val todo = todo0
      val deprecated = deprecated0
      val note = note0
      val example = example0
      val constructor = constructor0
      val inheritDiagram = inheritDiagram0
      val contentDiagram = contentDiagram0
      val groupDesc = groupDesc0
      val group =
        group0 match {
          case Some(
                Body(List(Paragraph(Chain(List(Summary(Text(groupId)))))))
              ) =>
            Some(groupId.toString.trim)
          case _ => None
        }
      val groupPrio = groupPrio0 flatMap { case (group, body) =>
        try {
          body match {
            case Body(List(Paragraph(Chain(List(Summary(Text(prio))))))) =>
              List(group -> prio.trim.toInt)
            case _ => List()
          }
        } catch {
          case _: java.lang.NumberFormatException => List()
        }
      }
      val groupNames = groupNames0 flatMap { case (group, body) =>
        body match {
          case Body(List(Paragraph(Chain(List(Summary(Text(name)))))))
              if (!name.trim.contains("\n")) =>
            List(group -> (name.trim))
          case _ => List()
        }
      }

      override val shortDescription: Option[Text] =
        shortDescription0.lastOption collect {
          case Body(List(Paragraph(Chain(List(Summary(Text(e)))))))
              if !e.trim.contains("\n") =>
            Text(e)
        }

      override val hideImplicitConversions: List[String] =
        hideImplicitConversions0 flatMap {
          case Body(List(Paragraph(Chain(List(Summary(Text(e)))))))
              if !e.trim.contains("\n") =>
            List(e)
          case _ => List()
        }
    }

  private val endOfText = '\u0003'
  private val endOfLine = '\u000A'

  /**
   * Something that should not have happened, happened, and Scaladoc should exit.
   */
  private def oops(msg: String): Nothing =
    sys.error("program logic: " + msg)

  /**
   * The body of a line, dropping the (optional) start star-marker,
   * one leading whitespace and all trailing whitespace.
   */
  private val CleanCommentLine =
    new Regex("""(?:\s*\*\s?)?(.*)""")

  /**
   * Dangerous HTML tags that should be replaced by something safer,
   * such as wiki syntax, or that should be dropped.
   */
  private val DangerousTags =
    new Regex("""<(/?(div|ol|ul|li|h[1-6]|p))( [^>]*)?/?>|<!--.*-->""")

  /**
   * Maps a dangerous HTML tag to a safe wiki replacement, or an empty string
   * if it cannot be salvaged.
   */
  private def htmlReplacement(mtch: Regex.Match): String =
    mtch.group(1) match {
      case "p" | "div" => "\n\n"
      case "h1" => "\n= "
      case "/h1" => " =\n"
      case "h2" => "\n== "
      case "/h2" => " ==\n"
      case "h3" => "\n=== "
      case "/h3" => " ===\n"
      case "h4" | "h5" | "h6" => "\n==== "
      case "/h4" | "/h5" | "/h6" => " ====\n"
      case "li" => "\n *  - "
      case _ => ""
    }

  /**
   * Javadoc tags that should be replaced by something useful, such as wiki
   * syntax, or that should be dropped.
   */
  private val JavadocTags =
    new Regex(
      """\{\@(code|docRoot|linkplain|link|literal|value)\p{Zs}*([^}]*)\}"""
    )

  /**
   * Maps a javadoc tag to a useful wiki replacement, or an empty string if it cannot be salvaged.
   */
  private def javadocReplacement(mtch: Regex.Match): String = {
    mtch.group(1) match {
      case "code" => "`" + mtch.group(2) + "`"
      case "docRoot" => ""
      case "link" => "[[" + mtch.group(2) + "]]"
      case "linkplain" => "[[" + mtch.group(2) + "]]"
      case "literal" => "`" + mtch.group(2) + "`"
      case "value" => "`" + mtch.group(2) + "`"
      case _ => ""
    }
  }

  /**
   * Safe HTML tags that can be kept.
   */
  private val SafeTags =
    new Regex(
      """((&\w+;)|(&#\d+;)|(</?(abbr|acronym|address|area|a|bdo|big|blockquote|br|button|b|caption|cite|code|col|colgroup|dd|del|dfn|em|fieldset|form|hr|img|input|ins|i|kbd|label|legend|link|map|object|optgroup|option|param|pre|q|samp|select|small|span|strong|sub|sup|table|tbody|td|textarea|tfoot|th|thead|tr|tt|var)( [^>]*)?/?>))"""
    )

  private val safeTagMarker = '\u000E'

  /**
   * A Scaladoc tag not linked to a symbol and not followed by text
   */
  private val SingleTagRegex =
    new Regex("""\s*@(\S+)\s*""")

  /**
   * A Scaladoc tag not linked to a symbol. Returns the name of the tag, and the rest of the line.
   */
  private val SimpleTagRegex =
    new Regex("""\s*@(\S+)\s+(.*)""")

  /**
   * A Scaladoc tag linked to a symbol. Returns the name of the tag, the name
   * of the symbol, and the rest of the line.
   */
  private val SymbolTagRegex =
    new Regex(
      """\s*@(param|tparam|throws|groupdesc|groupname|groupprio)\s+(\S*)\s*(.*)"""
    )

  /**
   * The start of a Scaladoc code block
   */
  private val CodeBlockStartRegex =
    new Regex("(.*?)((?:\\{\\{\\{)|(?:\u000E<pre(?: [^>]*)?>\u000E))(.*)")

  /**
   * The end of a Scaladoc code block
   */
  private val CodeBlockEndRegex =
    new Regex("(.*?)((?:\\}\\}\\})|(?:\u000E</pre>\u000E))(.*)")

  /**
   * A key used for a tag map. The key is built from the name of the tag and
   * from the linked symbol if the tag has one.
   * Equality on tag keys is structural.
   */
  private sealed abstract class TagKey {
    def name: String
  }

  private final case class SimpleTagKey(name: String) extends TagKey
  private final case class SymbolTagKey(name: String, symbol: String)
      extends TagKey

  private val TrailingWhitespaceRegex = """\s+$""".r
  def parseComment(
      comment: String,
      defines: collection.Map[String, String] = Map.empty
  ): Comment = {
    parseAtSymbol(HtmlConverter.convert(expandDefines(comment, defines)))
  }

  def extractDefines(
      docstring: String
  ): Iterator[(String, String)] = {
    for {
      (start, end) <- ScaladocUtils.tagIndex(docstring).iterator
      if ScaladocUtils.startsWithTag(docstring, start, "@define")
      List(tag, key, value) <- List(
        docstring
          .substring(start, end)
          .split("\\s", 3)
          .toList
      )
      if tag == "@define"
      cleaned = value.linesIterator.map(cleanLine).mkString("\n").trim
    } yield key -> cleaned
  }

  private val DefineReferenceRegex = "\\$([a-zA-Z]+)".r
  def expandDefines(
      in: String,
      defines: collection.Map[String, String]
  ): String = {
    val m = DefineReferenceRegex.pattern.matcher(in)
    val out = new StringBuilder()
    var start = 0
    while (m.find()) {
      out.append(in.substring(start, m.start()))
      start = m.end()
      val key = in.substring(m.start() + 1, m.end())
      defines.get(key) match {
        case Some(value) => out.append(value)
        case None => out.append(m.group())
      }
    }
    out.append(in.substring(start))
    out.toString()
  }

  def cleanLine(line: String): String = {
    // Remove trailing whitespaces
    TrailingWhitespaceRegex.replaceAllIn(line, "") match {
      case CleanCommentLine(ctl) => ctl
      case tl => tl
    }
  }

  /**
   * Parses a raw comment string into a `Comment` object.
   * @param comment The expanded comment string (including start and end markers) to be parsed.
   * @param pos     The position of the comment in source.
   */
  def parseAtSymbol(
      comment: String,
      pos: Position = Position.None
      //      site: Symbol = NoSymbol
  ): Comment = {

    /**
     * The cleaned raw comment as a list of lines. Cleaning removes comment
     * start and end markers, line start markers  and unnecessary whitespace.
     */
    def clean(comment: String): List[String] = {
      val strippedComment = comment.trim.stripPrefix("/*").stripSuffix("*/")
      val safeComment = DangerousTags.replaceAllIn(
        strippedComment,
        { mtch =>
          java.util.regex.Matcher.quoteReplacement(htmlReplacement(mtch))
        }
      )
      val javadoclessComment = JavadocTags.replaceAllIn(
        safeComment,
        { mtch =>
          java.util.regex.Matcher.quoteReplacement(javadocReplacement(mtch))
        }
      )
      val markedTagComment =
        SafeTags.replaceAllIn(
          javadoclessComment,
          { mtch =>
            java.util.regex.Matcher
              .quoteReplacement(
                "" + safeTagMarker + mtch.matched + safeTagMarker
              )
          }
        )
      markedTagComment.linesIterator.toList.map(cleanLine)
    }

    /**
     * Parses a comment (in the form of a list of lines) to a `Comment`
     * instance, recursively on lines. To do so, it splits the whole comment
     * into main body and tag bodies, then runs the `WikiParser` on each body
     * before creating the comment instance.
     *
     * @param docBody     The body of the comment parsed until now.
     * @param tags        All tags parsed until now.
     * @param lastTagKey  The last parsed tag, or `None` if the tag section hasn't started. Lines that are not tagged
     *                    are part of the previous tag or, if none exists, of the body.
     * @param remaining   The lines that must still recursively be parsed.
     * @param inCodeBlock Whether the next line is part of a code block (in which no tags must be read).
     */
    def parse0(
        docBody: StringBuilder,
        tags: Map[TagKey, List[String]],
        lastTagKey: Option[TagKey],
        remaining: List[String],
        inCodeBlock: Boolean
    ): Comment =
      remaining match {

        case CodeBlockStartRegex(before, marker, after) :: ls
            if (!inCodeBlock) =>
          if (!before.trim.isEmpty && !after.trim.isEmpty)
            parse0(
              docBody,
              tags,
              lastTagKey,
              before :: marker :: after :: ls,
              inCodeBlock = false
            )
          else if (!before.trim.isEmpty)
            parse0(
              docBody,
              tags,
              lastTagKey,
              before :: marker :: ls,
              inCodeBlock = false
            )
          else if (!after.trim.isEmpty)
            parse0(
              docBody,
              tags,
              lastTagKey,
              marker :: after :: ls,
              inCodeBlock = true
            )
          else
            lastTagKey match {
              case Some(key) =>
                val value =
                  ((tags get key): @unchecked) match {
                    case Some(b :: bs) => (b + endOfLine + marker) :: bs
                    case None =>
                      oops("lastTagKey set when no tag exists for key")
                  }
                parse0(
                  docBody,
                  tags + (key -> value),
                  lastTagKey,
                  ls,
                  inCodeBlock = true
                )
              case None =>
                parse0(
                  docBody append endOfLine append marker,
                  tags,
                  lastTagKey,
                  ls,
                  inCodeBlock = true
                )
            }

        case CodeBlockEndRegex(before, marker, after) :: ls => {
          if (!before.trim.isEmpty && !after.trim.isEmpty)
            parse0(
              docBody,
              tags,
              lastTagKey,
              before :: marker :: after :: ls,
              inCodeBlock = true
            )
          if (!before.trim.isEmpty)
            parse0(
              docBody,
              tags,
              lastTagKey,
              before :: marker :: ls,
              inCodeBlock = true
            )
          else if (!after.trim.isEmpty)
            parse0(
              docBody,
              tags,
              lastTagKey,
              marker :: after :: ls,
              inCodeBlock = false
            )
          else
            lastTagKey match {
              case Some(key) =>
                val value =
                  ((tags get key): @unchecked) match {
                    case Some(b :: bs) => (b + endOfLine + marker) :: bs
                    case None =>
                      oops("lastTagKey set when no tag exists for key")
                  }
                parse0(
                  docBody,
                  tags + (key -> value),
                  lastTagKey,
                  ls,
                  inCodeBlock = false
                )
              case None =>
                parse0(
                  docBody append endOfLine append marker,
                  tags,
                  lastTagKey,
                  ls,
                  inCodeBlock = false
                )
            }
        }

        case SymbolTagRegex(name, sym, body) :: ls if (!inCodeBlock) => {
          val key = SymbolTagKey(name, sym)
          val value = body :: tags.getOrElse(key, Nil)
          parse0(docBody, tags + (key -> value), Some(key), ls, inCodeBlock)
        }

        case SimpleTagRegex(name, body) :: ls if (!inCodeBlock) => {
          val key = SimpleTagKey(name)
          val value = body :: tags.getOrElse(key, Nil)
          parse0(docBody, tags + (key -> value), Some(key), ls, inCodeBlock)
        }

        case SingleTagRegex(name) :: ls if (!inCodeBlock) => {
          val key = SimpleTagKey(name)
          val value = "" :: tags.getOrElse(key, Nil)
          parse0(docBody, tags + (key -> value), Some(key), ls, inCodeBlock)
        }

        case line :: ls if (lastTagKey.isDefined) => {
          val newtags = if (!line.isEmpty || inCodeBlock) {
            val key = lastTagKey.get
            val value =
              ((tags get key): @unchecked) match {
                case Some(b :: bs) => (b + endOfLine + line) :: bs
                case None => oops("lastTagKey set when no tag exists for key")
              }
            tags + (key -> value)
          } else tags
          parse0(docBody, newtags, lastTagKey, ls, inCodeBlock)
        }

        case line :: ls => {
          if (docBody.nonEmpty) docBody append endOfLine
          docBody append line
          parse0(docBody, tags, lastTagKey, ls, inCodeBlock)
        }

        case Nil => {
          // Take the {inheritance, content} diagram keys aside, as it doesn't need any parsing
          val inheritDiagramTag = SimpleTagKey("inheritanceDiagram")
          val contentDiagramTag = SimpleTagKey("contentDiagram")

          val inheritDiagramText: List[String] =
            tags.get(inheritDiagramTag) match {
              case Some(list) => list
              case None => List.empty
            }

          val contentDiagramText: List[String] =
            tags.get(contentDiagramTag) match {
              case Some(list) => list
              case None => List.empty
            }

          val stripTags = List(
            inheritDiagramTag,
            contentDiagramTag,
            SimpleTagKey("template"),
            SimpleTagKey("documentable")
          )
          val tagsWithoutDiagram =
            tags.filterNot(pair => stripTags.contains(pair._1))

          val bodyTags: mutable.Map[TagKey, List[Body]] =
            mutable.Map(tagsWithoutDiagram.map { case (key, tag) =>
              key -> tag.map(parseWikiAtSymbol(_, pos))
            }.toSeq: _*)

          def oneTag(
              key: SimpleTagKey,
              filterEmpty: Boolean = true
          ): Option[Body] =
            (bodyTags.remove(key): @unchecked) match {
              case Some(r :: _) if !(filterEmpty && r.blocks.isEmpty) =>
                Some(r)
              case _ => None
            }

          def allTags(key: SimpleTagKey): List[Body] =
            (bodyTags remove key)
              .getOrElse(Nil)
              .filterNot(_.blocks.isEmpty)
              .reverse

          def allSymsOneTag(
              key: TagKey,
              filterEmpty: Boolean = true
          ): Map[String, Body] = {
            val keys: Seq[SymbolTagKey] =
              bodyTags.keys.toSeq flatMap {
                case stk: SymbolTagKey if (stk.name == key.name) => Some(stk)
                case stk: SimpleTagKey if (stk.name == key.name) =>
                  None
                case _ => None
              }
            val pairs: Seq[(String, Body)] =
              for (key <- keys) yield {
                val bs = (bodyTags remove key).get
                (key.symbol, bs.head)
              }
            Map.empty[String, Body] ++ (if (filterEmpty)
                                          pairs.filterNot(_._2.blocks.isEmpty)
                                        else pairs)
          }

          createComment(
            body0 = Some(parseWikiAtSymbol(docBody.toString, pos)),
            authors0 = allTags(SimpleTagKey("author")),
            see0 = allTags(SimpleTagKey("see")),
            result0 = oneTag(SimpleTagKey("return")),
            throws0 = allSymsOneTag(SimpleTagKey("throws")),
            valueParams0 = allSymsOneTag(SimpleTagKey("param")),
            typeParams0 = allSymsOneTag(SimpleTagKey("tparam")),
            version0 = oneTag(SimpleTagKey("version")),
            since0 = oneTag(SimpleTagKey("since")),
            todo0 = allTags(SimpleTagKey("todo")),
            deprecated0 =
              oneTag(SimpleTagKey("deprecated"), filterEmpty = false),
            note0 = allTags(SimpleTagKey("note")),
            example0 = allTags(SimpleTagKey("example")),
            constructor0 = oneTag(SimpleTagKey("constructor")),
            inheritDiagram0 = inheritDiagramText,
            contentDiagram0 = contentDiagramText,
            group0 = oneTag(SimpleTagKey("group")),
            groupDesc0 = allSymsOneTag(SimpleTagKey("groupdesc")),
            groupNames0 = allSymsOneTag(SimpleTagKey("groupname")),
            groupPrio0 = allSymsOneTag(SimpleTagKey("groupprio")),
            hideImplicitConversions0 =
              allTags(SimpleTagKey("hideImplicitConversion")),
            shortDescription0 = allTags(SimpleTagKey("shortDescription"))
          )
        }
      }

    parse0(
      new StringBuilder(comment.size),
      Map.empty,
      None,
      clean(comment),
      inCodeBlock = false
    )

  }

  /**
   * Parses a string containing wiki syntax into a `Comment` object.
   * Note that the string is assumed to be clean:
   *  - Removed Scaladoc start and end markers.
   *  - Removed start-of-line star and one whitespace afterwards (if present).
   *  - Removed all end-of-line whitespace.
   *  - Only `endOfLine` is used to mark line endings.
   */
  def parseWikiAtSymbol(
      string: String,
      pos: Position
      //      site: Symbol
  ): Body =
    new WikiParser(
      string,
      pos
      //      site
    ).document()

  /**
   * TODO
   *
   * @author Ingo Maier
   * @author Manohar Jonnalagedda
   * @author Gilles Dubochet
   */
  final class WikiParser(
      val buffer: String,
      pos: Position
      //      site: Symbol
  ) extends CharReader(buffer) { wiki =>
    var summaryParsed = false

    // TODO: Convert to Char
    private val TableCellStart = "|"

    def document(): Body = {
      val blocks = new mutable.ListBuffer[Block]
      while (char != endOfText) blocks += block()
      Body(blocks.toList)
    }

    /* BLOCKS */

    /**
     * {{{ block ::= code | title | hrule | listBlock | table | para  }}}
     */
    def block(): Block = {
      if (checkSkipInitWhitespace("{{{"))
        code()
      else if (checkSkipInitWhitespace('='))
        title()
      else if (checkSkipInitWhitespace("----"))
        hrule()
      else if (checkList)
        listBlock()
      else if (checkTableRow)
        table()
      else {
        para()
      }
    }

    /**
     * listStyle ::= '-' spc | '1.' spc | 'I.' spc | 'i.' spc | 'A.' spc | 'a.' spc
     * Characters used to build lists and their constructors
     */
    val listStyles: Map[String, Seq[Block] => Block] =
      Map[
        String,
        (Seq[Block] => Block)
      ]( // TODO Should this be defined at some list companion?
        "- " -> (UnorderedList(_)),
        "1. " -> (OrderedList(_, "decimal")),
        "I. " -> (OrderedList(_, "upperRoman")),
        "i. " -> (OrderedList(_, "lowerRoman")),
        "A. " -> (OrderedList(_, "upperAlpha")),
        "a. " -> (OrderedList(_, "lowerAlpha"))
      )

    /**
     * Checks if the current line is formed with more than one space and one the listStyles
     */
    def checkList: Boolean =
      (countWhitespace > 0) && (listStyles.keys exists {
        checkSkipInitWhitespace(_)
      })

    /**
     * {{{
     * nListBlock ::= nLine { mListBlock }
     *      nLine ::= nSpc listStyle para '\n'
     * }}}
     * Where n and m stand for the number of spaces. When `m > n`, a new list is nested.
     */
    def listBlock(): Block = {

      /**
       * Consumes one list item block and returns it, or None if the block is
       * not a list or a different list.
       */
      def listLine(indent: Int, style: String): Option[Block] =
        if (countWhitespace > indent && checkList)
          Some(listBlock())
        else if (countWhitespace != indent || !checkSkipInitWhitespace(style))
          None
        else {
          jumpWhitespace()
          jump(style)
          val p = Paragraph(parseInline(isInlineEnd = false))
          blockEnded("end of list line")
          Some(p)
        }

      /**
       * Consumes all list item blocks (possibly with nested lists) of the
       * same list and returns the list block.
       */
      def listLevel(indent: Int, style: String): Block = {
        val lines = mutable.ListBuffer.empty[Block]
        var line: Option[Block] = listLine(indent, style)
        while (line.isDefined) {
          lines += line.get
          line = listLine(indent, style)
        }
        val constructor = listStyles(style)
        constructor(lines)
      }

      val indent = countWhitespace
      val style = (listStyles.keys find { checkSkipInitWhitespace(_) })
        .getOrElse(listStyles.keys.head)
      listLevel(indent, style)
    }

    def code(): Block = {
      jumpWhitespace()
      jump("{{{")
      val str = readUntil("}}}")
      if (char == endOfText)
        reportError(pos, "unclosed code block")
      else
        jump("}}}")
      blockEnded("code block")
      Code(normalizeIndentation(str))
    }

    /**
     * {{{ title ::= ('=' inline '=' | "==" inline "==" | ...) '\n' }}}
     */
    def title(): Block = {
      jumpWhitespace()
      val inLevel = repeatJump('=')
      val text = parseInline(check("=" * inLevel))
      val outLevel = repeatJump('=', inLevel)
      if (inLevel != outLevel)
        reportError(pos, "unbalanced or unclosed heading")
      blockEnded("heading")
      Title(text, inLevel)
    }

    /**
     * {{{ hrule ::= "----" { '-' } '\n' }}}
     */
    def hrule(): Block = {
      jumpWhitespace()
      repeatJump('-')
      blockEnded("horizontal rule")
      HorizontalRule()
    }

    /**
     * Starts and end with a cell separator matching the minimal row || and all other possible rows
     */
    private val TableRow = """^\|.*\|$""".r

    /* Checks for a well-formed table row */
    private def checkTableRow = {
      check(TableCellStart) && {
        val newlineIdx = buffer.indexOf('\n', offset)
        newlineIdx != -1 &&
        TableRow.findFirstIn(buffer.substring(offset, newlineIdx)).isDefined
      }
    }

    /**
     * {{{
     * table         ::= headerRow '\n' delimiterRow '\n' dataRows '\n'
     * content       ::= inline-content
     * row           ::= '|' { content '|' }+
     * headerRow     ::= row
     * dataRows      ::= row*
     * align         ::= ':' '-'+ | '-'+ | '-'+ ':' | ':' '-'+ ':'
     * delimiterRow :: = '|' { align '|' }+
     * }}}
     */
    def table(): Block = {

      /* Helpers */

      def peek(tag: String): Unit = {
        val peek: String = buffer.substring(offset)
        val limit = 60
        val limitedPeek = peek.substring(0, limit min peek.length)
        println(s"peek: $tag: '$limitedPeek'")
      }

      /* Accumulated state */

      var header: Option[Row] = None

      val rows = mutable.ListBuffer.empty[Row]

      val cells = mutable.ListBuffer.empty[Cell]

      def finalizeCells(): Unit = {
        if (cells.nonEmpty) {
          rows += Row(cells.toList)
        }
        cells.clear()
      }

      def finalizeHeaderCells(): Unit = {
        if (cells.nonEmpty) {
          if (header.isDefined) {
            reportError(pos, "more than one table header")
          } else {
            header = Some(Row(cells.toList))
          }
        }
        cells.clear()
      }

      val escapeChar = "\\"

      /* Poor man's negative lookbehind */
      def checkInlineEnd =
        (check(TableCellStart) && !check(escapeChar, -1)) || check("\n")

      def decodeEscapedCellMark(text: String) =
        text.replace(escapeChar + TableCellStart, TableCellStart)

      def isEndOfText = char == endOfText

      def isStartMarkNewline = check(TableCellStart + endOfLine)

      def skipStartMarkNewline() = jump(TableCellStart + endOfLine)

      def isStartMark = check(TableCellStart)

      def skipStartMark() = jump(TableCellStart)

      def contentNonEmpty(content: Inline) = content != Text("")

      /**
       * @param cellStartMark The char indicating the start or end of a cell
       * @param finalizeRow   Function to invoke when the row has been fully parsed
       */
      def parseCells(cellStartMark: String, finalizeRow: () => Unit): Unit = {
        def jumpCellStartMark() = {
          if (!jump(cellStartMark)) {
            peek(s"Expected $cellStartMark")
            sys.error(s"Precondition violated: Expected $cellStartMark.")
          }
        }

        val startPos = offset

        jumpCellStartMark()

        val content = Paragraph(
          parseInline(
            isInlineEnd = checkInlineEnd,
            textTransform = decodeEscapedCellMark
          )
        )

        parseCells0(content :: Nil, finalizeRow, startPos, offset)
      }

      // Continue parsing a table row.
      //
      // After reading inline content the following conditions will be encountered,
      //
      //    Case : Next Chars
      //    ..................
      //      1  : end-of-text
      //      2  : '|' '\n'
      //      3  : '|'
      //      4  : '\n'
      //
      // Case 1.
      // State : End of text
      // Action: Store the current contents, close the row, report warning, stop parsing.
      //
      // Case 2.
      // State : The cell separator followed by a newline
      // Action: Store the current contents, skip the cell separator and newline, close the row, stop parsing.
      //
      // Case 3.
      // State : The cell separator not followed by a newline
      // Action: Store the current contents, skip the cell separator, continue parsing the row.
      //
      @tailrec def parseCells0(
          contents: List[Block],
          finalizeRow: () => Unit,
          progressPreParse: Int,
          progressPostParse: Int
      ): Unit = {

        def storeContents() = cells += Cell(contents.reverse)

        val startPos = offset

        // The ordering of the checks ensures the state checks are correct.
        if (progressPreParse == progressPostParse) {
          peek("no-progress-table-row-parsing")
          sys.error("No progress while parsing table row")
        } else if (isEndOfText) {
          // peek("1: end-of-text")
          // Case 1
          storeContents()
          finalizeRow()
          reportError(pos, "unclosed table row")
        } else if (isStartMarkNewline) {
          // peek("2: start-mark-new-line/before")
          // Case 2
          storeContents()
          finalizeRow()
          skipStartMarkNewline()
          // peek("2: start-mark-new-line/after")
        } else if (isStartMark) {
          // peek("3: start-mark")
          // Case 3
          storeContents()
          skipStartMark()
          val content = parseInline(
            isInlineEnd = checkInlineEnd,
            textTransform = decodeEscapedCellMark
          )
          // TrailingCellsEmpty produces empty content
          val accContents =
            if (contentNonEmpty(content)) Paragraph(content) :: Nil else Nil
          parseCells0(accContents, finalizeRow, startPos, offset)
        } else {
          // Case π√ⅈ
          // When the impossible happens leave some clues.
          reportError(pos, "unexpected table row markdown")
          peek("parseCell0")
          storeContents()
          finalizeRow()
        }
      }

      /* Parsing */

      jumpWhitespace()

      parseCells(TableCellStart, () => finalizeHeaderCells())

      while (checkTableRow) {
        val initialOffset = offset

        parseCells(TableCellStart, () => finalizeCells())

        /* Progress should always be made */
        if (offset == initialOffset) {
          peek("no-progress-table-parsing")
          sys.error("No progress while parsing table")
        }
      }

      /* Finalize */

      /* Structural consistency checks and coercion */

      // https://github.github.com/gfm/#tables-extension-
      // TODO: The header row must match the delimiter row in the number of cells. If not, a table will not be recognized:
      // TODO: Break at following block level element: The table is broken at the first empty line, or beginning of another block-level structure:
      // TODO: Do not return a table when: The header row must match the delimiter row in the number of cells. If not, a table will not be recognized

      if (cells.nonEmpty) {
        reportError(pos, s"Parsed and unused content: $cells")
      }
      assert(header.isDefined, "table header was not parsed")
      val enforcedCellCount = header.get.cells.size

      def applyColumnCountConstraint(
          row: Row,
          defaultCell: Cell,
          rowType: String
      ): Row = {
        if (row.cells.size == enforcedCellCount)
          row
        else if (row.cells.size > enforcedCellCount) {
          val excess = row.cells.size - enforcedCellCount
          reportError(
            pos,
            s"Dropping $excess excess table $rowType cells from row."
          )
          Row(row.cells.take(enforcedCellCount))
        } else {
          val missing = enforcedCellCount - row.cells.size
          Row(row.cells ++ List.fill(missing)(defaultCell))
        }
      }

      // TODO: Abandon table parsing when the delimiter is missing instead of fixing and continuing.
      val (delimiterRow, dataRows) =
        rows.toList match {
          case Nil =>
            reportError(pos, "Fixing missing delimiter row")
            (Row(Cell(Paragraph(Text("-")) :: Nil) :: Nil), Nil)
          case head :: tail =>
            (head, tail)
        }

      if (delimiterRow.cells.isEmpty)
        sys.error("TODO: Handle table with empty delimiter row")

      val constrainedDelimiterRow = applyColumnCountConstraint(
        delimiterRow,
        delimiterRow.cells(0),
        "delimiter"
      )

      val constrainedDataRows =
        dataRows.map(applyColumnCountConstraint(_, Cell(Nil), "data"))

      /* Convert the row following the header row to column options */

      val leftAlignmentPattern = "^:?-++$".r
      val centerAlignmentPattern = "^:-++:$".r
      val rightAlignmentPattern = "^-++:$".r

      import ColumnOption._
      /* Encourage user to fix by defaulting to least ignorable fix. */
      val defaultColumnOption = ColumnOptionRight
      val columnOptions = constrainedDelimiterRow.cells.map {
        alignmentSpecifier =>
          alignmentSpecifier.blocks match {
            // TODO: Parse the second row without parsing inline markdown
            // TODO: Save pos when delimiter row is parsed and use here in reported errors
            case Paragraph(Text(as)) :: Nil =>
              as.trim match {
                case leftAlignmentPattern(_*) => ColumnOptionLeft
                case centerAlignmentPattern(_*) => ColumnOptionCenter
                case rightAlignmentPattern(_*) => ColumnOptionRight
                case x =>
                  reportError(pos, s"Fixing invalid column alignment: $x")
                  defaultColumnOption
              }
            case x =>
              reportError(pos, s"Fixing invalid column alignment: $x")
              defaultColumnOption
          }
      }

      if (check("\n", -1)) {
        prevChar()
      } else {
        peek("expected-newline-missing")
        sys.error("table parsing left buffer in unexpected state")
      }

      blockEnded("table")
      Table(header.get, columnOptions, constrainedDataRows)
    }

    /**
     * {{{ para ::= inline '\n' }}}
     */
    def para(): Block = {
      val p =
        if (summaryParsed)
          Paragraph(parseInline(isInlineEnd = false))
        else {
          val s = summary()
          val r =
            if (checkParaEnded()) List(s)
            else List(s, parseInline(isInlineEnd = false))
          summaryParsed = true
          Paragraph(Chain(r))
        }
      while (char == endOfLine && char != endOfText) nextChar()
      p
    }

    /* INLINES */

    val OPEN_TAG: Regex = "^<([A-Za-z]+)( [^>]*)?(/?)>$".r
    val CLOSE_TAG: Regex = "^</([A-Za-z]+)>$".r
    private def readHTMLFrom(begin: HtmlTag): String = {
      val list = mutable.ListBuffer.empty[String]
      val stack = mutable.ListBuffer.empty[String]

      begin.close match {
        case Some(HtmlTag(CLOSE_TAG(s))) =>
          stack += s
        case _ =>
          return ""
      }

      while ({
        val str = readUntil { char == safeTagMarker || char == endOfText }
        nextChar()

        list += str

        str match {
          case OPEN_TAG(s, _, standalone) => {
            if (standalone != "/") {
              stack += s
            }
          }
          case CLOSE_TAG(s) => {
            if (s == stack.last) {
              stack.remove(stack.length - 1)
            }
          }
          case _ => ;
        }
        (stack.nonEmpty && char != endOfText)
      }) ()

      list mkString ""
    }

    private def parseInline(
        isInlineEnd: => Boolean,
        textTransform: String => String = identity
    ): Inline = {

      def inline0(): Inline = {
        if (char == safeTagMarker) {
          val tag = htmlTag()
          HtmlTag(tag.data + readHTMLFrom(tag))
        } else if (check("'''")) bold()
        else if (check("''")) italic()
        else if (check("`")) monospace()
        else if (check("__")) underline()
        else if (check("^")) superscript()
        else if (check(",,")) subscript()
        else if (check("[[")) link()
        else {
          val str = readUntil {
            char == safeTagMarker || check("''") || char == '`' || check(
              "__"
            ) || char == '^' || check(
              ",,"
            ) || check(
              "[["
            ) || isInlineEnd || checkParaEnded() || char == endOfLine
          }
          Text(textTransform(str))
        }
      }

      val inlines: List[Inline] = {
        val iss = mutable.ListBuffer.empty[Inline]
        iss += inline0()
        while (!isInlineEnd && !checkParaEnded()) {
          val skipEndOfLine = if (char == endOfLine) {
            nextChar()
            true
          } else {
            false
          }

          val current = inline0()
          (iss.last, current) match {
            case (Text(t1), Text(t2)) if skipEndOfLine =>
              iss.update(iss.length - 1, Text(t1 + endOfLine + t2))
            case (_, i2) if skipEndOfLine =>
              iss ++= List(Text(endOfLine.toString), i2)
            case _ => iss += current
          }
        }
        iss.toList
      }

      inlines match {
        case Nil => Text("")
        case i :: Nil => i
        case is => Chain(is)
      }

    }

    def htmlTag(): HtmlTag = {
      jump(safeTagMarker)
      val read = readUntil(safeTagMarker)
      if (char != endOfText) jump(safeTagMarker)
      HtmlTag(read)
    }

    def bold(): Inline = {
      jump("'''")
      val i = parseInline(check("'''"))
      jump("'''")
      Bold(i)
    }

    def italic(): Inline = {
      jump("''")
      val i = parseInline(check("''"))
      jump("''")
      Italic(i)
    }

    def monospace(): Inline = {
      jump("`")
      val i = parseInline(check("`"))
      jump("`")
      Monospace(i)
    }

    def underline(): Inline = {
      jump("__")
      val i = parseInline(check("__"))
      jump("__")
      Underline(i)
    }

    def superscript(): Inline = {
      jump("^")
      val i = parseInline(check("^"))
      if (jump("^")) {
        Superscript(i)
      } else {
        Chain(Seq(Text("^"), i))
      }
    }

    def subscript(): Inline = {
      jump(",,")
      val i = parseInline(check(",,"))
      jump(",,")
      Subscript(i)
    }

    def summary(): Inline = {
      val i = parseInline(checkSentenceEnded())
      Summary(
        if (jump("."))
          Chain(List(i, Text(".")))
        else
          i
      )
    }

    def link(): Inline = {
      jump("[[")
      val parens = 2 + repeatJump('[')
      val stop = "]" * parens
      val link = readUntil { check(stop) }
      jump(stop)

      link match {
        case LinkPattern(link, _, _, title) =>
          Link(link, Option(title) map (Text.apply) getOrElse Text(link))
        case text =>
          Link(text, Text(text))
      }
    }

    /* UTILITY */

    /**
     * {{{ eol ::= { whitespace } '\n' }}}
     */
    def blockEnded(blockType: String): Unit = {
      if (char != endOfLine && char != endOfText) {
        reportError(
          pos,
          "no additional content on same line after " + blockType
        )
        jumpUntil(endOfLine)
      }
      while (char == endOfLine) nextChar()
    }

    /**
     *  Eliminates the (common) leading spaces in all lines, based on the first line
     *  For indented pieces of code, it reduces the indent to the least whitespace prefix:
     *    {{{
     *       indented example
     *       another indented line
     *       if (condition)
     *         then do something;
     *       ^ this is the least whitespace prefix
     *    }}}
     */
    def normalizeIndentation(_code: String): String = {

      val code =
        _code
          .replaceAll("\\s+$", "")
          .dropWhile(_ == '\n') // right-trim + remove all leading '\n'
      val lines = code.split("\n")

      // maxSkip - size of the longest common whitespace prefix of non-empty lines
      val nonEmptyLines = lines.filter(_.trim.nonEmpty)
      val maxSkip =
        if (nonEmptyLines.isEmpty) 0
        else nonEmptyLines.map(line => line.prefixLength(_ == ' ')).min

      // remove common whitespace prefix
      lines
        .map(line => if (line.trim.nonEmpty) line.substring(maxSkip) else line)
        .mkString("\n")
    }

    def checkParaEnded(): Boolean = {
      (char == endOfText) ||
      ((char == endOfLine) && {
        val poff = offset
        nextChar() // read EOL
        val ok = {
          checkSkipInitWhitespace(endOfLine) ||
          checkSkipInitWhitespace('=') ||
          checkSkipInitWhitespace("{{{") ||
          checkList ||
          check(TableCellStart) ||
          checkSkipInitWhitespace('\u003D')
        }
        offset = poff
        ok
      })
    }

    def checkSentenceEnded(): Boolean = {
      (char == '.') && {
        val poff = offset
        nextChar() // read '.'
        val ok = char == endOfText || char == endOfLine || isWhitespace(char)
        offset = poff
        ok
      }
    }

    def reportError(pos: Position, message: String): Unit = ()
  }

  sealed class CharReader(buffer: String) { reader =>

    var offset: Int = 0
    def char: Char =
      if (offset >= buffer.length) endOfText else buffer charAt offset

    final def nextChar(): Unit = {
      offset += 1
    }

    final def prevChar(): Unit = {
      offset -= 1
    }

    final def check(chars: String): Boolean = {
      val poff = offset
      val ok = jump(chars)
      offset = poff
      ok
    }

    final def check(chars: String, checkOffset: Int): Boolean = {
      val poff = offset
      offset += checkOffset
      val ok = jump(chars)
      offset = poff
      ok
    }

    def checkSkipInitWhitespace(c: Char): Boolean = {
      val poff = offset
      jumpWhitespace()
      val ok = jump(c)
      offset = poff
      ok
    }

    def checkSkipInitWhitespace(chars: String): Boolean = {
      val poff = offset
      jumpWhitespace()
      val (ok0, chars0) =
        if (chars.charAt(0) == ' ')
          (offset > poff, chars substring 1)
        else
          (true, chars)
      val ok = ok0 && jump(chars0)
      offset = poff
      ok
    }

    def countWhitespace: Int = {
      var count = 0
      val poff = offset
      while (isWhitespace(char) && char != endOfText) {
        nextChar()
        count += 1
      }
      offset = poff
      count
    }

    /* JUMPERS */

    /**
     * jumps a character and consumes it
     * @return true only if the correct character has been jumped
     */
    final def jump(ch: Char): Boolean = {
      if (char == ch) {
        nextChar()
        true
      } else false
    }

    /**
     * jumps all the characters in chars, consuming them in the process.
     * @return true only if the correct characters have been jumped
     */
    final def jump(chars: String): Boolean = {
      var index = 0
      while (
        index < chars.length && char == chars.charAt(index) && char != endOfText
      ) {
        nextChar()
        index += 1
      }
      index == chars.length
    }

    final def repeatJump(c: Char, max: Int = Int.MaxValue): Int = {
      var count = 0
      while (jump(c) && count < max) count += 1
      count
    }

    final def jumpUntil(ch: Char): Int = {
      var count = 0
      while (char != ch && char != endOfText) {
        nextChar()
        count += 1
      }
      count
    }

    final def jumpUntil(pred: => Boolean): Int = {
      var count = 0
      while (!pred && char != endOfText) {
        nextChar()
        count += 1
      }
      count
    }

    def jumpWhitespace(): Int = jumpUntil(!isWhitespace(char))

    def jumpWhitespaceOrNewLine(): Int = jumpUntil(!isWhitespaceOrNewLine(char))

    /* READERS */

    final def readUntil(c: Char): String = {
      withRead {
        while (char != c && char != endOfText) {
          nextChar()
        }
      }
    }

    final def readUntil(chars: String): String = {
      assert(chars.length > 0)
      withRead {
        val c = chars.charAt(0)
        while (!check(chars) && char != endOfText) {
          nextChar()
          while (char != c && char != endOfText) nextChar()
        }
      }
    }

    final def readUntil(pred: => Boolean): String = {
      withRead {
        while (char != endOfText && !pred) {
          nextChar()
        }
      }
    }

    private def withRead(read: => Unit): String = {
      val start = offset
      read
      buffer.substring(start, offset)
    }

    /* CHARS CLASSES */

    def isWhitespace(c: Char): Boolean = c == ' ' || c == '\t'

    def isWhitespaceOrNewLine(c: Char): Boolean = isWhitespace(c) || c == '\n'
  }

}
